package com.code.soulnetty.modbus;

import com.code.soulnetty.modbus.biz.ChannelInfo;
import com.code.soulnetty.modbus.client.ModbusClientManager;
import com.code.soulnetty.modbus.client.NettyClient;
import com.code.soulnetty.modbus.client.message.ModbusTcpReadRequest;
import com.code.soulnetty.modbus.client.message.ProtocolRequest;
import com.code.soulnetty.modbus.client.message.ProtocolRequestWrap;
import com.code.soulnetty.modbus.client.message.ProtocolResponse;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.websocket.server.PathParam;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

import static com.code.soulnetty.modbus.biz.Utils.*;

/**
 * modbus客户端访问入口
 *
 * @author lyf
 * @date 2022-12-16 23:50
 */
@Slf4j
@RestController
public class ModbusController {

    private static final int RETRY_COUNT = 2;

    @Autowired
    private ModbusClientManager clientManager;

    @GetMapping("/send")
    public String sendData(@PathParam("data") String data) {
        int period = MapUtils.getInteger(Maps.newHashMap(), PERIOD, 10);
        long timestamp = System.currentTimeMillis();
        int periodTime = Math.toIntExact(timestamp / 1000 / period * period);
        List<ModbusTcpReadRequest> requests = Lists.newArrayList();

        ChannelInfo channelInfo = new ChannelInfo();
        NettyClient client = clientManager.client(channelInfo);
        if (client == null) {
            return "client is null";
        }

        //连接并校验
        if (checkAndConnect(channelInfo, timestamp, periodTime, requests, client)) {
            // todo... 连接失败业务处理
            // callBack.onCheck(ChannelStatus.OFFLINE);
            return "connect fail";
        } else {
            // todo... 连接成功业务处理
            // 当前通道连接成功
            // callBack.onCheck(ChannelStatus.ONLINE);
        }
        //连接成功，发送数据
        for (ProtocolRequest protocolRequest : requests) {
            try {
                CompletableFuture<ProtocolResponse> future = client.send(new ProtocolRequestWrap(timestamp, periodTime, protocolRequest));
                // 当netty服务端响应回来之后，会在 handler 中给 future 塞值，这里就可以取出来了
                future.thenAccept(response -> {
                    log.info("监听到结果返回，res: {}", response);
                });
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        return "ok";
    }

    /**
     * 校验当前是否连接成功
     * 如果没有连接，先一次进行连接，如果连接成功，直接返回，如果连接不成功，进行可达性校验，确定不可达则直接返回，
     * 可达则需要进行两次重新连接，最后进行判断是否连接成功
     *
     * @param timestamp  时间戳
     * @param periodTime 周期时间
     * @param requests   请求数据
     * @param client     客户端信息
     * @return true 连接不成功，false 连接成功
     */
    private boolean checkAndConnect(ChannelInfo channelInfo, long timestamp, int periodTime,
                                    List<ModbusTcpReadRequest> requests, NettyClient client) {
        //每次发送数据之前，先判断是否链接还在
        if (!client.isConnected()) {
            //先连接
            connect(channelInfo, client);
            //连接成功返回
            if (client.isConnected()) {
                return false;
            }
            //连接不上，是否可达性校验
            boolean checkMyselfResult = checkReachable(channelInfo, client);
            if (!checkMyselfResult) {
                return false;
            }
            //重练2次
            retryConn(channelInfo, client);
        }
        //重练两次都不通
        if (!client.isConnected()) {
            log.info("[CHANNEL] [FETCH_OFFLINE] {},{},{},{},{}", channelInfo.getChannelId(), periodTime,
                    (System.currentTimeMillis() - timestamp), requests.size(), (timestamp - channelInfo.getLastRunTime()));
            return true;
        }
        return false;
    }

    private void connect(ChannelInfo channelInfo, NettyClient client) {
        //先连接
        Map<String, String> props = channelInfo.getConfig();
        //重试次数
        int retryCount = MapUtils.getInteger(props, RETRY_COUNT_STR, DEFAULT_RETRY_COUNT);
        //轮询间隔
        long reqIntl = MapUtils.getLong(props, REQ_INTL, DEFAULT_REQ_INTL);
        //超时时间
        long timeOut = MapUtils.getLong(props, TIME_OUT, DEFAULT_TIME_OUT);
        try {
            client.connect(new InetSocketAddress(channelInfo.getIp(), channelInfo.getPort()), retryCount, timeOut, reqIntl);
        } catch (Exception e) {
            log.error("channel connect exception, {}", e.getMessage());
        }
    }

    /**
     * 重练两次
     *
     * @param client
     */
    private void retryConn(ChannelInfo channelInfo, NettyClient client) {
        try {
            for (int i = 0; i < RETRY_COUNT; i++) {
                client.reConn(channelInfo);
                if (client.isConnected()) {
                    break;
                }
            }
        } catch (Exception e) {
            log.error("[CHANNEL] [RE_CONN_ERROR] {},{}", client.getRemoteAddress(), e.getMessage());
        }
    }

    /**
     * 确定可达性
     *
     * @param client 客户端信息
     * @return false 网络通，true 不需检测，或者不通
     */
    private boolean checkReachable(ChannelInfo channelInfo, NettyClient client) {
        Integer check = MapUtils.getInteger(channelInfo.getConfig(), "CHECK_TIME_OUT");
        if (check == null) {
            return true;
        }
        try {
            InetSocketAddress remoteAddress = client.getRemoteAddress();
            if (remoteAddress == null) {
                return true;
            }
            return !remoteAddress.getAddress().isReachable(check);
        } catch (IOException e) {
            log.error("channel check, {}", e.getMessage());
        }
        return true;
    }
}
